#ifdef USE_N2N
#include <ctime>
#include "n2n.h"
#include <rtos.h>
#include "n2nClass.h"
#include "speck.h"

extern "C"
{
void send_register_super(n2n_edge_t *eee);
void send_query_peer(n2n_edge_t *eee, const n2n_mac_t dst_mac);
int supernode_connect(n2n_edge_t *eee);
int supernode_disconnect(n2n_edge_t *eee);
int fetch_and_eventually_process_data(n2n_edge_t *eee, SOCKET sock,
                                      uint8_t *pktbuf, uint16_t *expected, uint16_t *position,
                                      time_t now);
int resolve_check(n2n_resolve_parameter_t *param, uint8_t resolution_request, time_t now);
int edge_init_routes(n2n_edge_t *eee, n2n_route_t *routes, uint16_t num_routes);
}

int n2nClass::thread() {

    int rc;
    tuntap_dev tuntap;           /* a tuntap device */
    n2n_edge_t *eee;             /* single instance for this program */
    n2n_edge_conf_t conf;        /* generic N2N edge config */
    n2n_tuntap_priv_config_t ec; /* config used for standalone program execution */
    uint8_t runlevel = 0;        /* bootstrap: runlevel */
    uint8_t seek_answer = 1;     /*            expecting answer from supernode */
    time_t now, last_action = 0; /*            timeout */
    macstr_t mac_buf;            /*            output mac address */
    fd_set socket_mask;          /*            for supernode answer */
    struct timeval wait_time
            {
            };                            /*            timeout for sn answer */
    peer_info_t *scan, *scan_tmp; /*            supernode iteration */

    uint16_t expected = sizeof(uint16_t);
    uint16_t position = 0;
    uint8_t pktbuf[N2N_SN_PKTBUF_SIZE + sizeof(uint16_t)]; /* buffer + prepended buffer length in case of tcp */

#ifndef WIN32
    struct passwd *pw = nullptr;
#endif
#ifdef HAVE_LIBCAP
    cap_t caps;
#endif
#ifdef WIN32
    initWin32();
#endif

    /* Defaults */
    edge_init_conf_defaults(&conf);
    memset(&ec, 0, sizeof(ec));
    ec.mtu = DEFAULT_MTU;
    ec.daemon = 0; /* By default run in daemon mode. */

#ifndef WIN32
//    if (((pw = getpwnam("n2n")) != nullptr) ||
//        ((pw = getpwnam("nobody")) != nullptr)) {
//        ec.userid = pw->pw_uid;
//        ec.groupid = pw->pw_gid;
//    }
#endif

#ifdef WIN32
    ec.tuntap_dev_name[0] = '\0';
    ec.metric = 0;
#else
    snprintf(ec.tuntap_dev_name, sizeof(ec.tuntap_dev_name), N2N_EDGE_DEFAULT_DEV_NAME);
#endif
    snprintf(ec.netmask, sizeof(ec.netmask), N2N_EDGE_DEFAULT_NETMASK);

    // --- additional crypto setup; REVISIT: move to edge_init()?
    // payload
    if (conf.transop_id == N2N_TRANSFORM_ID_NULL) {
        if (conf.encrypt_key) {
            // make sure that AES is default cipher if key only (and no cipher) is specified
            traceEvent(TRACE_WARNING, "switching to AES as key was provided");
            conf.transop_id = N2N_TRANSFORM_ID_AES;
        }
    }
    // user auth
    if (conf.shared_secret /* containing private key only so far*/) {
        // if user-password auth and no federation public key provided, use default
        if (!conf.federation_public_key) {
            conf.federation_public_key = (n2n_private_public_key_t *) calloc(1, sizeof(n2n_private_public_key_t));
            if (conf.federation_public_key) {
                traceEvent(TRACE_WARNING,
                           "using default federation public key; FOR TESTING ONLY, usage of a custom federation name and key (-P) is highly recommended!");
                generate_private_key(*(conf.federation_public_key), (char *) &FEDERATION_NAME[1]);
                generate_public_key(*(conf.federation_public_key), *(conf.federation_public_key));
            }
        }
        // calculate public key and shared secret
        if (conf.federation_public_key) {
            traceEvent(TRACE_NORMAL, "using username and password for edge authentication");
            bind_private_key_to_username(*(conf.shared_secret), (char *) conf.dev_desc);
            conf.public_key = (n2n_private_public_key_t *) calloc(1, sizeof(n2n_private_public_key_t));
            if (conf.public_key)
                generate_public_key(*conf.public_key, *(conf.shared_secret));
            generate_shared_secret(*(conf.shared_secret), *(conf.shared_secret), *(conf.federation_public_key));
            // prepare (first 128 bit) for use as key
            conf.shared_secret_ctx = (he_context_t *) calloc(1, sizeof(speck_context_t));
            speck_init((speck_context_t **) &(conf.shared_secret_ctx), (const unsigned char *) *(conf.shared_secret),
                       128);
        }
        // force header encryption
        if (conf.header_encryption != HEADER_ENCRYPTION_ENABLED) {
            traceEvent(TRACE_NORMAL, "enabling header encryption for edge authentication");
            conf.header_encryption = HEADER_ENCRYPTION_ENABLED;
        }
    }

#if defined(HAVE_OPENSSL_1_1)
    traceEvent(TRACE_NORMAL, "using %s", OpenSSL_version(0));
#endif

    //    traceEvent(TRACE_NORMAL, "using compression: %s.", compression_str(conf.compression));
    //    traceEvent(TRACE_NORMAL, "using %s cipher.", transop_str(conf.transop_id));

    /* Random seed */
    n2n_srand(n2n_seed());

#ifndef WIN32
    /* If running suid root then we need to setuid before using the force. */
    //    if (setuid(0) != 0)
    //        traceEvent(TRACE_ERROR, "unable to become root [%u/%s]", errno, strerror(errno));
    /* setgid(0); */
#endif

    if (conf.encrypt_key && !strcmp((char *) conf.community_name, conf.encrypt_key))
        traceEvent(TRACE_WARNING, "community and encryption key must differ, otherwise security will be compromised");

    if ((eee = edge_init(&conf, &rc)) == nullptr) {
        traceEvent(TRACE_ERROR, "failed in edge_init");
        exit(1);
    }

    memcpy(&(eee->tuntap_priv_conf), &ec, sizeof(ec));

    if ((0 == strcmp("static", eee->tuntap_priv_conf.ip_mode)) ||
        ((eee->tuntap_priv_conf.ip_mode[0] == '\0') && (eee->tuntap_priv_conf.ip_addr[0] != '\0'))) {
        traceEvent(TRACE_NORMAL, "use manually set IP address");
        eee->conf.tuntap_ip_mode = TUNTAP_IP_MODE_STATIC;
    } else if (0 == strcmp("dhcp", eee->tuntap_priv_conf.ip_mode)) {
        traceEvent(TRACE_NORMAL, "obtain IP from other edge DHCP services");
        eee->conf.tuntap_ip_mode = TUNTAP_IP_MODE_DHCP;
    } else {
        traceEvent(TRACE_NORMAL, "automatically assign IP address by supernode");
        eee->conf.tuntap_ip_mode = TUNTAP_IP_MODE_SN_ASSIGN;
    }

    // mini main loop for bootstrap, not using main loop code because some of its mechanisms do not fit in here
    // for the sake of quickly establishing connection. REVISIT when a more elegant way to re-use main loop code
    // is found

    // find at least one supernode alive to faster establish connection
    // exceptions:
    if ((HASH_COUNT(eee->conf.supernodes) <= 1) || (eee->conf.connect_tcp) || (eee->conf.shared_secret)) {
        // skip the initial supernode ping
        traceEvent(TRACE_DEBUG, "skip PING to supernode");
        runlevel = 2;
    }

    eee->last_sup = 0; /* if it wasn't zero yet */
    eee->curr_sn = eee->conf.supernodes;
    supernode_connect(eee);
    while (runlevel < 5) {

        now = time(nullptr);

        // we do not use switch-case because we also check for 'greater than'

        if (runlevel == 0) { /* PING to all known supernodes */
            last_action = now;
            eee->sn_pong = 0;
            // (re-)initialize the number of max concurrent pings (decreases by calling send_query_peer)
            eee->conf.number_max_sn_pings = NUMBER_SN_PINGS_INITIAL;
            send_query_peer(eee, null_mac);
            traceEvent(TRACE_NORMAL, "send PING to supernodes");
            runlevel++;
        }

        if (runlevel == 1) { /* PING has been sent to all known supernodes */
            if (eee->sn_pong) {
                // first answer
                eee->sn_pong = 0;
                sn_selection_sort(&(eee->conf.supernodes));
                eee->curr_sn = eee->conf.supernodes;
                supernode_connect(eee);
                traceEvent(TRACE_NORMAL, "received first PONG from supernode [%s]", eee->curr_sn->ip_addr);
                runlevel++;
            } else if (last_action <= (now - BOOTSTRAP_TIMEOUT)) {
                // timeout
                runlevel--;
                // skip waiting for answer to directly go to send PING again
                seek_answer = 0;
                traceEvent(TRACE_DEBUG, "PONG timeout");
            }
        }

        // by the way, have every later PONG cause the remaining (!) list to be sorted because the entries
        // before have already been tried; as opposed to initial PONG, do not change curr_sn
        if (runlevel > 1) {
            if (eee->sn_pong) {
                eee->sn_pong = 0;
                if (eee->curr_sn->hh.next) {
                    sn_selection_sort((peer_info_t **) &(eee->curr_sn->hh.next));
                    traceEvent(TRACE_DEBUG, "received additional PONG from supernode");
                    // here, it is hard to determine from which one, so no details to output
                }
            }
        }

        if (runlevel == 2) { /* send REGISTER_SUPER to get auto ip address from a supernode */
            if (eee->conf.tuntap_ip_mode == TUNTAP_IP_MODE_SN_ASSIGN) {
                last_action = now;
                eee->sn_wait = 1;
                send_register_super(eee);
                runlevel++;
                traceEvent(TRACE_NORMAL, "send REGISTER_SUPER to supernode [%s] asking for IP address",
                           eee->curr_sn->ip_addr);
            } else {
                runlevel += 2; /* skip waiting for TUNTAP IP address */
                traceEvent(TRACE_DEBUG, "skip auto IP address assignment");
            }
        }

        if (runlevel == 3) { /* REGISTER_SUPER to get auto ip address from a sn has been sent */
            if (!eee->sn_wait) { /* TUNTAP IP address received */
                runlevel++;
                traceEvent(TRACE_NORMAL, "received REGISTER_SUPER_ACK from supernode for IP address assignment");
                // it should be from curr_sn, but we can't determine definitely here, so no details to output
            } else if (last_action <= (now - BOOTSTRAP_TIMEOUT)) {
                // timeout, so try next supernode
                if (eee->curr_sn->hh.next)
                    eee->curr_sn = (peer_info_t *) eee->curr_sn->hh.next;
                else
                    eee->curr_sn = eee->conf.supernodes;
                supernode_connect(eee);
                runlevel--;
                // skip waiting for answer to directly go to send REGISTER_SUPER again
                seek_answer = 0;
                traceEvent(TRACE_DEBUG, "REGISTER_SUPER_ACK timeout");
            }
        }

        if (runlevel == 4) { /* configure the TUNTAP device, including routes */
            if (tuntap_open(&tuntap, eee->tuntap_priv_conf.tuntap_dev_name, eee->tuntap_priv_conf.ip_mode,
                            eee->tuntap_priv_conf.ip_addr, eee->tuntap_priv_conf.netmask,
                            eee->tuntap_priv_conf.device_mac, eee->tuntap_priv_conf.mtu
#ifdef WIN32
                    ,
                    eee->tuntap_priv_conf.metric
#endif
            ) < 0)
                exit(1);
            memcpy(&eee->device, &tuntap, sizeof(tuntap));
            traceEvent(TRACE_NORMAL, "created local tap device IP: %s, Mask: %s, MAC: %s",
                       eee->tuntap_priv_conf.ip_addr,
                       eee->tuntap_priv_conf.netmask,
                       macaddr_str(mac_buf, eee->device.mac_addr));
            // routes
            if (edge_init_routes(eee, eee->conf.routes, eee->conf.num_routes) < 0) {
                traceEvent(TRACE_ERROR, "routes setup failed");
                exit(1);
            }
            runlevel = 5;
            // no more answers required
            seek_answer = 0;
        }

        // we usually wait for some answer, there however are exceptions when going back to a previous runlevel
        if (seek_answer) {
            FD_ZERO(&socket_mask);
            FD_SET(eee->sock, &socket_mask);
            wait_time.tv_sec = BOOTSTRAP_TIMEOUT;
            wait_time.tv_usec = 0;

            if (select(eee->sock + 1, &socket_mask, nullptr, nullptr, &wait_time) > 0) {
                if (FD_ISSET(eee->sock, &socket_mask)) {

                    fetch_and_eventually_process_data(eee, eee->sock,
                                                      pktbuf, &expected, &position,
                                                      now);
                }
            }
        }
        seek_answer = 1;

        resolve_check(eee->resolve_parameter, 0 /* no intermediate resolution requirement at this point */, now);
    }
    // allow a higher number of pings for first regular round of ping
    // to quicker get an initial 'supernode selection criterion overview'
    eee->conf.number_max_sn_pings = NUMBER_SN_PINGS_INITIAL;
    // shape supernode list; make current one the first on the list
    HASH_ITER(hh, eee->conf.supernodes, scan, scan_tmp) {
        if (scan == eee->curr_sn)
            sn_selection_criterion_good(&(scan->selection_criterion));
        else
            sn_selection_criterion_default(&(scan->selection_criterion));
    }
    sn_selection_sort(&(eee->conf.supernodes));
    // do not immediately ping again, allow some time
    eee->last_sweep = now - SWEEP_TIME + 2 * BOOTSTRAP_TIMEOUT;
    eee->sn_wait = 1;
    eee->last_register_req = 0;

#ifdef __linux__
    signal(SIGPIPE, SIG_IGN);
    signal(SIGTERM, term_handler);
    signal(SIGINT, term_handler);
#endif
#ifdef WIN32
    SetConsoleCtrlHandler(term_handler, TRUE);
#endif

    keep_on_running = 1;
    eee->keep_running = &keep_on_running;
    traceEvent(TRACE_NORMAL, "edge started");
    rc = run_edge_loop(eee);
    print_edge_stats(eee);

#ifdef HAVE_LIBCAP
    /* Before completing the cleanup, regain the capabilities as some
     * cleanup tasks require them (e.g. routes cleanup). */
    cap_set_flag(caps, CAP_EFFECTIVE, num_cap, cap_values, CAP_SET);

    if (cap_set_proc(caps) != 0)
        traceEvent(TRACE_WARNING, "could not regain the capabilities [%s]\n", strerror(errno));

    cap_free(caps);
#endif

    /* Cleanup */
    edge_term_conf(&eee->conf);
    tuntap_close(&eee->device);
    edge_term(eee);

#ifdef WIN32
    destroyWin32();
#endif

    return (rc);
}

int n2nClass::run(uint32_t tick) {
    return 0;
}

int n2nClass::begin(uint32_t tick) {
    Rtos::create_thread_run("n2n", 3000, PriorityNormal, (void *) &n2nClass::thd_loop, this);
    return 0;
}

int n2nClass::diag(uint32_t tick) {
    return 0;
}
#ifdef __PLATFORMIO_BUILD_DEBUG__
int n2nClass::dev_test(uint32_t tick) {
    return 0;
}
#endif
void n2nClass::thd_loop(void *arg) {
    lwip_socket_thread_init();

    setTraceLevel(10);

    /* Random seed */
    n2n_srand(n2n_seed());
    quick_edge_init("n2n0", "mynetwork", "mysecret", "DE:AD:BE:EF:01:10", "192.168.100.100", "39.106.53.251:1234",
                    &keep_on_running);
}


int n2nClass::keep_on_running;
#endif
